package ru.mirent;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.path.json.JsonPath;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.*;

/**
 * Класс для работы с Json, требуется разобрать на отдельные классы-действия
 */
public class JsonUtils {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static {
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    private JsonUtils() {
    }

    /**
     * TODO Перенести в отдельный класс-функцию
     *
     * @param object
     * @return
     */
    public static String toJson(Object object) {
        try {
            return OBJECT_MAPPER.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * TODO Перенести в отдельный класс-функцию
     *
     * @param object
     * @return
     */
    public static String toPrettyJson(Object object) {
        try {
            return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * TODO Перенести в отдельный класс-функцию
     *
     * @param object
     * @return
     */
    public static String prettiObject(Object object) {
        Map<String, Object> map = new HashMap<>();

        try {
            if (object instanceof String) {
                map = OBJECT_MAPPER.readValue(object.toString(), new TypeReference<Map<String, Object>>() {
                });
            } else {
                map = OBJECT_MAPPER.readValue(toJson(object), new TypeReference<Map<String, Object>>() {
                });
            }
        } catch (IOException e) {
            return e.getLocalizedMessage();
        }

        Map<String, Object> sortMap = new TreeMap<>(map);

        return prettifyObject(sortMap, -1, new StringBuilder());
    }

    /**
     * Рекурсивный обход содержимого объекта многовложенных List и Map (JSON, переведенный в объект).
     * TODO Перенести в отдельный класс-функцию, подумать о добавлени параметра отступа INDENT
     *
     * @param object исходный объект для разбора
     * @param num    указывается значение '-1', требуется для работы рекурсии
     * @param sb     указывается новый объект {@link StringBuilder}, требуется для работы рекурсии
     * @return преобразованый в текст объект
     */
    private static String prettifyObject(Object object, int num, StringBuilder sb) {
        System.out.println(num);
        final int INDENT = 4;
        if (object instanceof List) {
            List<?> list = (List<?>) object;
            num++;
            for (int i = 0; i < list.size(); i++) {
                prettifyObject(list.get(i), num, sb);
            }
        } else if (object instanceof Map) {
            num++;
            sb.append(System.lineSeparator());
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
                sb.append(getTabs(num))
                        .append(entry.getKey().toString().toUpperCase(Locale.ROOT))
                        .append(":");
                prettifyObject(entry.getValue(), num, sb);
                sb.append(System.lineSeparator());
            }
        } else if (object instanceof String) {
            num++;
            String prettiJson = prettify(object.toString(), INDENT);
            if (prettiJson != null) {
                sb.append(System.lineSeparator())
                        .append(addTabsToNewLine(prettiJson, num));
            } else {
                sb.append(System.lineSeparator())
                        .append(getTabs(num))
                        .append(object);
            }
        } else {
            num++;
            sb.append(System.lineSeparator())
                    .append(getTabs(num))
                    .append(object);
        }

        return sb.toString().trim();
    }

    /**
     * Сформировать указанное количество символов TAB
     * TODO Перенести в отдельный класс-функцию и переделать для передачи для различных символов
     *
     * @param num количество повторений
     * @return продублированный num-раз символ
     */
    private static StringBuilder getTabs(int num) {
        if (num <= 0) {
            return new StringBuilder();
        } else {
            final String TAB = "\t";
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < num; i++) {
                sb.append(TAB);
            }
            return sb;
        }
    }

    /**
     * Добавить отсупы ко всему переданному тексту.
     * TODO Перенести в отдельный класс-функцию и добавить передачу различных символов
     *
     * @param text текст для добавления отступов
     * @param num  количество отступов
     * @return форматированный текст
     */
    private static StringBuilder addTabsToNewLine(String text, int num) {
        StringBuilder sb = new StringBuilder();
        String[] lines = text.split(System.lineSeparator());
        for (int i = 0; i < lines.length; i++) {
            sb.append(getTabs(num))
                    .append(lines[i]);
            if (i < lines.length - 1) {
                sb.append(System.lineSeparator());
            }
        }

        return sb;
    }

    /**
     * Форматирование текста в вид с отступами.
     * TODO Перенести в отдельный класс-функцию
     *
     * @param text   исходный текст
     * @param indent отступ
     * @return отформатированный JSON
     */
    public static String prettify(String text, int indent) {
        try {
            JSONObject jsonObject = new JSONObject(text);
            return jsonObject.toString(indent);
        } catch (JSONException e) {
            return null;
        }
    }

    /**
     * Создать список путей ко всем элементам JSON с указанием их значений.
     * TODO Перенести в отдельный класс-функцию
     *
     * @param gPathQuery       начальный GPath путь, от которого начнется обработка JSON, для всего JSON указать ""
     * @param jsonPath         преобразованный в {@link JsonPath} текст JSON
     * @param mapQueryAndValue результирующая Map, используется в рекурсии, всегда равна новому объекту {Map}
     *                         соответствующих типов
     * @return Map, где Key будет путь к элементу в JSON, а Value - значение из JSON по даному пути
     */
    public static Map<String, Object> createPathToAllElementsInJson(String gPathQuery, JsonPath jsonPath,
                                                                    Map<String, Object> mapQueryAndValue) {
        try {
            Object object = jsonPath.get(gPathQuery);
            switch (object.getClass().getSimpleName()) {
                case "LinkedHashMap":
                    Map<?, ?> map = (LinkedHashMap<?, ?>) object;
                    for (Object obj : map.keySet()) {
                        if ("".equals(gPathQuery)) {
                            createPathToAllElementsInJson(gPathQuery + "." + obj, jsonPath, mapQueryAndValue);
                        } else {
                            createPathToAllElementsInJson(obj.toString(), jsonPath, mapQueryAndValue);
                        }
                    }
                    break;
                case "ArrayList":
                    List<?> list = (ArrayList<?>) object;
                    for (int i = 0; i < list.size(); i++) {
                        createPathToAllElementsInJson(gPathQuery + "[" + i + "]", jsonPath, mapQueryAndValue);
                    }
                    break;
                case "String":
                    mapQueryAndValue.put(gPathQuery, object);
                    break;
            }
        } catch (Exception ignore) {
        }

        return mapQueryAndValue;
    }

    /**
     * Рекурсивный обход содержимого объекта с выводом данных в виде текста.
     * Данный метод можно использовать в качестве заготовки для реализации различных способов обхода JSON,
     * преобразоавнных в List/Map и других объектов.
     * @param object объект для обхода
     * @param sb указывается новый объект {@link StringBuilder}, , требуется для работы рекурсии
     * @return {@link String} результат обхода
     */
    public static String recursiveTraversalObject(Object object, StringBuilder sb) {
        try {
            if (object instanceof List) {
                for (Object l : (List<?>) object) {
                    recursiveTraversalObject(l, sb);
                }
            } else if (object instanceof Map) {
                for (Map.Entry<String, Object> entry : ((Map<String, Object>) object).entrySet()) {
                    sb.append(System.lineSeparator())
                            .append(entry.getKey());
                    recursiveTraversalObject(entry.getValue(), sb);
                }
            } else if (object instanceof String) {
                sb.append(" = ")
                        .append(object);
            }
        } catch (Exception ignore) {
        }

        return sb.toString();
    }
}
